package org.jboss.seam.wiki.core.preferences;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.log4j.Logger;
import org.jboss.seam.wiki.core.dao.WikiPreferenceDao;
import org.jboss.seam.wiki.preferences.PreferenceProvider;
import org.jboss.seam.wiki.preferences.PreferenceValue;
import org.jboss.seam.wiki.preferences.PreferenceVisibility;
import org.jboss.seam.wiki.preferences.metamodel.PreferenceEntity;
import org.jboss.seam.wiki.preferences.metamodel.PreferenceRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xelnaga.defiler.domain.Account;

/**
 * Implementation for the wiki, loads and stores <tt>WikiPreferenceValue</tt> objects.
 * <p>
 * This implementation tries to be as smart as possible and supports multi-level preference value overrides.
 * If you load values, they are automatically resolved for system, user, and instance levels. If a value is
 * present at system level but you want values for users, this implementation creates and returns those
 * missing values. It will also persist them if you call <tt>store()</tt> and <tt>flush()</tt>.
 * </p>
 * <p>
 * System and user-level values are loaded and stored in the database with the <tt>entityManager</tt>
 * persistence context. be careful to only flush it if you want to after you retrieved and modified
 * values with this provider.
 * </p>
 * <p>
 * Instance-level values are not stored in the database, they are marshalled by converting <tt>WikiMacro</tt>
 * (the instance this provider understands) parameters.
 * </p>
 *
 * @author Christian Bauer
 */
@Component("preferenceProvider")
public class WikiPreferenceProvider implements PreferenceProvider<Account>, Serializable {

	private static final long serialVersionUID = -3581319571507851862L;

	protected static final Logger log = Logger.getLogger(WikiPreferenceProvider.class);

    @Autowired
    WikiPreferenceDao wikiPreferenceDao;
    @Autowired
    PreferenceRegistry preferenceRegistry;

    // Queue in current conversation until flush()
//    List<PreferenceValue> newValueHolders = new ArrayList<PreferenceValue>();

    public Set<PreferenceValue> loadValues(String preferenceEntityName,
    		Account user, List<PreferenceVisibility> visibilities) {
        log.debug("assembling preference values for '"
                    + preferenceEntityName
                    + "' and user '" + user + "'");

        PreferenceEntity entity = preferenceRegistry.getPreferenceEntitiesByName().get(preferenceEntityName);
        SortedSet<PreferenceValue> valueHolders = new TreeSet<PreferenceValue>();

        Set<PreferenceValue> systemValueHolders = null;
        if (visibilities.contains(PreferenceVisibility.SYSTEM)) {
            log.trace("retrieving SYSTEM preference values from database");
            systemValueHolders = loadSystemValues(preferenceEntityName);
            // Check if all SYSTEM-level properties were present in the database
            for (PreferenceEntity.Property systemVisibleProperty : entity.getPropertiesSystemVisible()) {
                WikiPreferenceValue newValueHolder = new WikiPreferenceValue(systemVisibleProperty);
                // If not, queue a new value holder (with a null "value") for that property and return it
                if (!systemValueHolders.contains(newValueHolder)) {
                    systemValueHolders.add(newValueHolder);
                }
            }
            valueHolders.addAll(systemValueHolders);
        }
        if(null != user) {
	        Set<PreferenceValue> userValueHolders;
	        if (visibilities.contains(PreferenceVisibility.USER)) {
	            log.trace("retrieving USER preference values from database");
	            userValueHolders = loadUserValues(preferenceEntityName, user);
	
	            // We need them when we iterate through missing properties next
	            if (systemValueHolders == null) {
	            	systemValueHolders = loadSystemValues(preferenceEntityName);
	            }
	
	            // Check if all USER-level properties were present in the database
	            for (PreferenceEntity.Property userVisibleProperty : entity.getPropertiesUserVisible()) {
	                WikiPreferenceValue newValueHolder = new WikiPreferenceValue(userVisibleProperty);
	
	                // If not, queue a new value for that property and return it
	                if (!userValueHolders.contains(newValueHolder)) {
	                    log.trace("creating new preference value for user, missing in database " + userVisibleProperty);
	                    userValueHolders.add(newValueHolder);
	                    newValueHolder.setUser(user);
	
	                    // We need a "value" for that property, let's check if we have a SYSTEM-level property we can take it from
	                    if (userVisibleProperty.getVisibility().contains(PreferenceVisibility.SYSTEM) &&
	                        systemValueHolders != null && systemValueHolders.contains(newValueHolder)) {
	                        for (PreferenceValue systemValue : systemValueHolders) {
	                            if (systemValue.equals(newValueHolder)) {
	                                log.trace("taking the initial value from the SYSTEM-level setting");
	                                newValueHolder.setValue(systemValue.getValue());
	                                newValueHolder.setDirty(false); // New value isn't dirty, by definition
	                            }
	                        }
	                    }
	                    // If we don't have a value, well, then it's null (this is a property with USER and no SYSTEM visibility)
	                }
	            }
	
	            // Override SYSTEM values
	            valueHolders.removeAll(userValueHolders);
	            valueHolders.addAll(userValueHolders);
	        }
        }
        return valueHolders;
    }

    public void storeValues(Set<PreferenceValue> valueHolders, Account user) {
        // TODO: We don't care about the arguments, maybe instance later on when we marshall stuff into WikiMacro params
        // The new ones need to be checked if they are dirty and manually persisted
//        for (PreferenceValue newPreferenceValue : newValueHolders) {
//            if (newPreferenceValue.isDirty()) {
//                log.debug("storing new preference value " + newPreferenceValue);
//                wikiPreferenceDAO.save(newPreferenceValue);
//            }
//        }
        // The old ones are automatically checked for dirty state by Hibernate
    }

    public void deleteUserPreferenceValues(Account user) {
        log.debug("deleting preferences of user: " + user);
        this.wikiPreferenceDao.deleteByUser(user.getPrimaryKey());
//        List<WikiPreferenceValue> values = this.wikiPreferenceDAO.getByUserList(user);
//            entityManager.createQuery(
//                            "select wp from WikiPreferenceValue wp" +
//                            " where wp.user = :user"
//                          ).setParameter("user", user)
//                           .getResultList();
//        for (WikiPreferenceValue value : values) {
//            entityManager.remove(value);
//        }
    }

    public void flush() {
        log.debug("flushing preference provider");
    }


    /* ######################################### IMPLEMENTATION DETAILS ######################################### */

    private Set<PreferenceValue> loadSystemValues(String entityName) {
        List<WikiPreferenceValue> values = this.wikiPreferenceDao.getSystemValues(entityName);
//            entityManager.createQuery(
//                            "select wp from WikiPreferenceValue wp" +
//                            " where wp.entityName = :name and wp.user is null"
//                          ).setParameter("name", entityName)
//                           .setHint("org.hibernate.cacheable", true)
//                           .getResultList();
        setPropertyReferences(values, entityName, PreferenceVisibility.SYSTEM);
        return new HashSet<PreferenceValue>(values);
    }

    private Set<PreferenceValue> loadUserValues(String entityName, Account user) {
        if (user.getPrimaryKey() == null) return Collections.emptySet();
        List<WikiPreferenceValue> values = wikiPreferenceDao.getUserValues(entityName, user.getPrimaryKey());
//            entityManager.createQuery(
//                            "select wp from WikiPreferenceValue wp" +
//                            " where wp.entityName = :name and wp.user = :user"
//                          ).setParameter("name", entityName)
//                           .setParameter("user", user)
//                           .setHint("org.hibernate.cacheable", true)
//                           .getResultList();
        setPropertyReferences(values, entityName, PreferenceVisibility.USER);
        return new HashSet<PreferenceValue>(values);
    }

    private void setPropertyReferences(List<WikiPreferenceValue> valueHolders, String entityName, PreferenceVisibility visibility) {
        Iterator<WikiPreferenceValue> it = valueHolders.iterator();
        while (it.hasNext()) {
            WikiPreferenceValue wikiPreferenceValue = it.next();
            if (!setPropertyReference(wikiPreferenceValue, entityName, visibility)) {
                 it.remove(); // Kick out the value if we couldn't set a reference to the metamodel property
            }
        }
    }

    private boolean setPropertyReference(WikiPreferenceValue value, String entityName, PreferenceVisibility visibility) {
        PreferenceEntity entity = preferenceRegistry.getPreferenceEntitiesByName().get(entityName);
        if (entity == null) {
            log.warn("orphaned preference value found in database, please clean up: " + value);
            return false;
        }
        PreferenceEntity.Property property = entity.getPropertiesByName().get(value.getPropertyName());
        if (property == null) {
            log.warn("orphaned preference value found in database, please clean up: " + value);
            return false;
        }
        if (!property.getVisibility().contains(visibility)) {
            log.warn("database visibility " + visibility  + " is not allowed by property, please clean up: " + value);
            return false;
        }
        value.setPreferenceProperty(property);
        return true;
    }


}
